package com.cv.infra.service.file;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.cv.framework.common.utils.encrypt.SHAUtil;
import com.cv.framework.common.utils.object.BeanUtils;
import com.cv.framework.mybatis.core.query.QueryWrapperX;
import com.cv.infra.api.file.dto.DynamicThreadDTO;
import com.cv.infra.controller.platform.file.vo.PlatformFileRespVO;
import com.cv.infra.dal.dataobject.file.InfraFileDO;
import com.cv.infra.dal.mapper.InfraFileMapper;
import com.cv.infra.service.dynamicThreadPool.DynamicThreadPoolService;
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.errors.MinioException;
import lombok.extern.slf4j.Slf4j;
import me.desair.tus.server.TusFileUploadService;
import me.desair.tus.server.exception.TusException;
import me.desair.tus.server.upload.UploadInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;

/**
 * 文件服务业务层接口实现类
 *
 * @author Charles_XDXD
 */
@Service
@Slf4j
public class FileServiceImpl implements FileService {

    @Resource
    private InfraFileMapper infraFileMapper;

    @Resource
    private TusFileUploadService tusFileUploadService;

    @Resource
    private DynamicThreadPoolService dynamicThreadPoolService;

    @Value("${os.win.localStorage.path}")
    private String localStorePath;

    @Resource
    private MinioClient minioClient;

    @Value("${minio.endpoint}")
    private String endpoint;

    @Value("${minio.bucket-name}")
    private String minioBucket;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void handleFileUpload(HttpServletRequest request, HttpServletResponse response) {
        log.info("[FileService]接收到上传文件请求");
        response.addHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "Location,Upload-Offset,Upload-Length");
        try {
            // Tus 上传
            tusFileUploadService.process(request, response);

            // 获取上传信息
            String uploadUri = request.getRequestURI();
            UploadInfo uploadInfo = tusFileUploadService.getUploadInfo(uploadUri);

            // 上传完毕后，处理合并数据
            if (uploadInfo != null && !uploadInfo.isUploadInProgress()) {
                processCompletedUpload(uploadInfo, request.getRequestURI());
            }

        } catch (Exception e) {
            log.error("[FileService]处理失败", e);
            throw new RuntimeException("文件处理失败", e);
        }
    }

    @Override
    public PlatformFileRespVO checkFileExist(String hash) {
        if (StrUtil.isEmpty(hash)) {
            return null;
        }

        QueryWrapperX<InfraFileDO> wrapper = new QueryWrapperX<InfraFileDO>()
                .eq("hash", hash);
        InfraFileDO infraFileDO = infraFileMapper.selectOne(wrapper);
        if (ObjUtil.isEmpty(infraFileDO)) {
            return null;
        }
        return BeanUtils.toBean(infraFileDO, PlatformFileRespVO.class);
    }

    @Override
    public void updateFileInfo(Long fileId, String path) {
        UpdateWrapper<InfraFileDO> wrapper = new UpdateWrapper<InfraFileDO>()
                .set("path", path)
                .eq("id", fileId);
        infraFileMapper.update(null, wrapper);
    }

    private void processCompletedUpload(UploadInfo uploadInfo, String uploadUrl) {
        // 获取临时文件路径
        Path tempFilePath = Paths.get(localStorePath, "tus");
        Path parPath = tempFilePath.resolve("uploads").resolve(uploadInfo.getId().getOriginalObject().toString());
        Path filePath = parPath.resolve(uploadInfo.getFileName());

        // 将合并后的文件重命名为原文件名称，并删除合并前的文件
        try (InputStream uploadedBytes = tusFileUploadService.getUploadedBytes(uploadUrl, null)) {
            Files.copy(uploadedBytes, filePath, StandardCopyOption.REPLACE_EXISTING);
        } catch (Exception e) {
            try {
                tusFileUploadService.deleteUpload(uploadUrl);
            } catch (IOException | TusException ee) {
                log.error("删除上传文件失败: {}", ee.getMessage());
            }
        }

        Map<String, String> metadata = uploadInfo.getMetadata();
        if (ObjUtil.isNotEmpty(metadata)) {
            String hash = metadata.get("hash");

            // 计算合并文件的哈希值并进行完整性校验
            String uploadFileHash;
            try (InputStream is = Files.newInputStream(filePath)) {
                uploadFileHash = SHAUtil.calcSHA256Hash(is);
            } catch (IOException e) {
                throw new RuntimeException("文件哈希值计算失败：", e);
            }
            if (!uploadFileHash.equals(hash)) {
                try {
                    Files.deleteIfExists(filePath);
                } catch (IOException e) {
                    throw new RuntimeException("文件删除失败");
                }
                log.error("[FileService] 上传文件校验失败");
                throw new RuntimeException("上传文件校验失败");
            }

            // 将文件信息存储到数据库中
            String randomFileName = IdUtil.fastSimpleUUID();
            String fileSuffix = uploadInfo.getFileName().substring(uploadInfo.getFileName().lastIndexOf(".") + 1);
            String fileName = randomFileName + "_" + System.currentTimeMillis() + "." + fileSuffix;
            InfraFileDO infraFileDO = new InfraFileDO();
            infraFileDO.setHash(uploadFileHash);
            infraFileDO.setType(uploadInfo.getFileMimeType());
            infraFileDO.setUploadType(metadata.get("uploadType"));
            infraFileDO.setOriginalName(uploadInfo.getFileName());
            infraFileDO.setFilename(fileName);
            infraFileDO.setSize(uploadInfo.getLength());
            // 先设置为 minio，在本地转码后，会上传到该平台。
            infraFileDO.setPlatform("minio");
            infraFileMapper.insert(infraFileDO);

            if ("video".equals(metadata.get("uploadType"))) {
                log.info("[FileService]视频上传完毕，开始转码");
                DynamicThreadDTO dynamicThreadDTO = new DynamicThreadDTO();
                dynamicThreadDTO.setId(infraFileDO.getId());
                dynamicThreadDTO.setFileName(fileName);
                dynamicThreadDTO.setFilePath(filePath.toString());
                dynamicThreadPoolService.executeConvertVideoTask(dynamicThreadDTO);
            } else {
                log.info("[FileService]文件上传完毕，开始转存到 MinIO");
                try (FileInputStream fileInputStream = new FileInputStream(filePath.toFile())) {
                    // 之所以多次一举，是为了让开发者能够知道，视频上传的时候，minioPath 是一个文件夹路径。而这里不是视频的逻辑处理，因此是一个文件路径。
                    String minioPath = endpoint + "/" + minioBucket + "/" + metadata.get("uploadType") + "/" + randomFileName + "/" + fileName;
                    String objectName = metadata.get("uploadType") + "/" + randomFileName + "/" + fileName;

                    boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(minioBucket).build());
                    if (!exists) {
                        minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioBucket).build());
                    }
                    PutObjectArgs args = PutObjectArgs.builder()
                            .bucket(minioBucket)
                            .object(objectName)
                            .stream(fileInputStream, uploadInfo.getLength(), -1)
                            .build();
                    minioClient.putObject(args);

                    // 更新文件信息中的路径为 MinIO 的路径
                    infraFileDO.setPath(minioPath);
                    infraFileMapper.updateById(infraFileDO);
                    log.info("[FileService] 文件成功转存到 MinIO，文件路径：{}", minioPath);
                } catch (MinioException | IOException | InvalidKeyException | NoSuchAlgorithmException e) {
                    log.error("[FileService] 文件上传到 MinIO 失败", e);
                    throw new RuntimeException("文件上传到 MinIO 失败", e);
                }
            }
        }
    }

}